在 Swift 环境下使用 JavaScriptCore 和本地代码交互

缘由

最近被问起这样的问题,所以把自己知道的写出来
包括 Web 端调用 Native 和 Native 调用 Web,算是记录一下

本文使用 Xcode 7.3 + Swift,最低兼容 iOS 8.0

这里查看源代码)是项目的完整代码,下载完成后,请执行 pod install

目录文件介绍

ViewController.swift

ViewController.swift 主要配合 Main.storyboard 绘制了 UI 元素
接下来绝大部分代码工作将在这个文件中进行
viewDidLoad方法中主要是让 webView 加载 Bundle 中的 html 文件

JavaScriptCoreDemo.html

这个文件主要用来模拟实际使用时我们会遇到的 web 页面

Web 端调用 iOS 端代码

我将其分为了如下步骤

为 WebView 注册代理,并实现代理方法

webView.delegate = self


extension ViewController : UIWebViewDelegate {
func webViewDidFinishLoad(webView: UIWebView) {
// Waiting
}
}

获取到当前 Web 页面中的上下文

func webViewDidFinishLoad(webView: UIWebView) {
    context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
    //    Waiting
}

注册回调

func webViewDidFinishLoad(webView: UIWebView) {

    context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext

    let callBack : @convention(block) (AnyObject?) -> Void = { [weak self] (paramFromJS) -> Void in
        //    Waiting
     }

    context?.setObject(unsafeBitCast(callBack, AnyObject.self), forKeyedSubscript: "callNative")
}

展示 Web 页面传递过来的的参数

最后我们需要通知自己原生代码已经接受到了 web 端的调用,并且输出一下 web 页面传递过来了参数

在 ViewController 类中,实现 fromJS 方法:

func fromJS(paramFromJS:AnyObject?) {

    Alert
        .showIn(self)
        .style(UIAlertControllerStyle.ActionSheet)
        .title("Call From JS")
        .message("检测到了来自 JS 的调用!")
        .addAction("我知道了", style: .Cancel, handler: nil)

    guard let paramFromJS = paramFromJS else {
        return
    }

    print("param from JavaScript is:")
    print(paramFromJS)

}

其中 Alert 类来自于 Halo.framework,用来通知自己原生代码已经接受到了 web 端的调用,对 UIAlertController 做了简单的封装,不必关心

在上一步中的 Waiting 注释处调用

let callBack : @convention(block) (AnyObject?) -> Void = { [weak self] (paramFromJS) -> Void in
    self?.fromJS(paramFromJS)    
}

这里的 self (也就是我们的 ViewController) 中被加上了 weak 修饰符,这是因为:

context?.setObject(unsafeBitCast(callBack, AnyObject.self), forKeyedSubscript: "callNative")

context 会强引用这个回调块,为了避免强引用,我们需要将 self 在 block 中声明为弱引用

至于 @convention(block) ,大家可以参看这里

PS: 还有一个问题,上面的代码大家是不是有点害怕?优雅的 Swift 怎么会出现 unsafeBitCast(callBack, AnyObject.self) 这样的代码!大家可以看看 这里
期待 iOS10 SDK 中 Swift 环境下,JSContext 可以实现下标访问

PS2: 另外,我在这里将 paramFromJS 声明为了 AnyObject? 类型,JavaScriptCore 是可以自动将 js 传递的参数做转换的,上面例子中,我们通过

print(paramFromJS!.dynamicType)

可以发现 paramFromJS 被转化为了__NSDictionaryM
关于 dynamicType,请看这里

运行程序

此时控制台输出:

模拟器:

iOS 端调用 Web 的 js 代码

可以分为两种方式

调用 Web 页面中已经实现好的 JS

我们的 web 页面中包含了一段已经写好的 JavaScript function:

function fromNative(args) {
    document.getElementById("fromNativeParan").innerHTML = "来自原生代码的参数为" + args;
};

我们可以通过 JavaScriptContext 直接对其进行调用:

在 ViewController 类中,实现 callJS 方法:

func callJS() {
    let params : [AnyObject]! = ["Hello JS! \(arc4random() % 10)"]
    context?.objectForKeyedSubscript("fromNative").callWithArguments(params)
}

arc4random() % 10 的作用是为了产生随机取以区分多次点击

在 rightBarButtonClick 中调用:

@IBAction func rightBarButtonClick(sender: UIBarButtonItem) {
    callJS()
}

web 页面产生了如下变化:

直接注入一段 JS 代码

在 ViewController 类中,实现 evaluateJS 方法:

func evaluateJS() {
context?.evaluateScript(“alert(\”你调用了 JS\”)”)
}

在 rightBarButtonClick 中调用:

@IBAction func rightBarButtonClick(sender: UIBarButtonItem) {
    evaluateJS()
}

效果如图:

PS: 实际上 evaluateJS 还可以小城下面这种形式:

func evaluateJS() {
    context?.evaluateScript("fromNative(' 你运行了一段JS')")
}

这时该方法的作用和 ViewController.callJS 就一样了

题外话

本来还想写点 WKWebView 的东西,但鉴于自己以前的使用经历,还是算了…
简单说一下使用 WKWebView 遇到的坑吧:

  • 无法通过点击 web 页面跳转到 AppStore,解决方案
  • 在淘宝页面时无法点击 web 页面上的按钮,解决方案
  • web 页面的 alert 在移动端上失效,解决方案

每个都是超级严重的 bug 有没有!
在产品经理面前直冒冷汗有没有!

相关链接